\chapter{高性能末级缓存``320 LLC''}\label{chap:RTLLLC}

{

\section{引言}
在传统的内存层次结构中，重内存负载下的访存需求性能很大程度上与LLC，即末级缓存相关。因为缓存的命中率一般情况下与缓存的容量呈正相关，LLC通常是整个缓存层次结构中容量最大的缓存，而重内存负载下，L1与L2缓存通常会出现缺失，只有LLC有足够的容量可以满足其命中需求。但是缓存的性能除了取决于命中率，还取决于延迟。因此优化LLC的内部结构，从而优化LLC的延迟是提升系统性能的重要手段。

虽然采用模拟器进行仿真\citep{qemu_2005}\citep{gem5_2011}\citep{gem5_2011}\citep{Sanchez_Kozyrakis_2013}已经成为了成为了当下体系结构研究领域中探索新架构的重要手段，但是当试图深入研究处理器微架构\citep{8342082}\citep{Owahid_John_2019}，尤其是LLC的微架构时，往往需要对微架构的各级流水线周期、队列的争用情况、面积等因素进行深入的研究，而模拟器难以实现如此细致的模拟。因此，一个RTL级的高性能末级缓存代码对于相关领域的研究十分重要。

但是可用于实验的高性能RTL级代码目前十分稀缺。首先在数量上，随着处理器技术的不断发展，处理器内部的微架构已经变得十分复杂，研究者想要自行构建一个现代的RTL级微处理器以进行实验变得十分困难，因此目前市面上的开源处理器数量较少。同时在性能上，目前的开源处理器距离现代化商业处理器的性能水平有明显差距，因此在这样的平台上难以发现与解决真实世界面临的前沿问题。

目前比较著名的两个开源RTL级处理器实验平台分别是伯克利大学所开发的ChipYard项目\citep{chipyard_2020}，以及中科院计算所所开发的“香山”项目\citep{micro2022xiangshan}，但是这两个项目的微结构设计仍然有大量的改进空间。

以LLC部分的代码为例，ChipYard中的缓存子项目Rocket Chip Inclusive Cache采用了硬件不友好的链表作为请求的调度队列，使得在硬件综合时单独的调度队列就会出现时序违例，无法运用于实际硬件。“香山”项目中的缓存子项目HuanCun没有实现请求的缓冲调度队列，使得在MSHR队列阻塞时无法暂存更多的新请求。同时这两个项目都是包含式的设计，同时采用了同组串行执行设计，使得其并不适合重负载下使用。

因此，为了构建一个现代化、高性能的RTL级处理器实验平台，“320 CPU”项目被启动，旨在设计一个便于研究人员进行实验和模拟，并将实验结果运用于实际处理器设计的实验平台。其中，“320 LLC”子项目也被一同提出，该子项目的目标是设计一个高性能的片上末级缓存，在其中采用各种先进技术以提升系统在重内存负载下的表现。

本文的第一项研究便是“320 LLC”的设计与实现。

\section{“320 LLC”总体设计}

图\ref{fig:llc}给出了本文所设计的320 LLC的整体框图。整体而言，320 LLC可以分为四大部分。

\begin{figure}[H]
    \centering
    \includegraphics[width=0.9\textwidth]{llc}
    \bicaption{\enspace LLC总体设计结构}{\enspace LLC Overall Structure}
    \label{fig:llc}
\end{figure}

第一部分是CPU与LLC交互的缓存总线。该缓存总线采用的是新型的“320”总线，图中是该总线的最简形态，总共有4个通道，分别是NewReq、SnpReq、SnpRsp、LLCRsp通道。其中，来自CPU的新请求从NewReq通道进入，并将请求缓冲到请求缓冲-调度队列（Req Array）内。而其余三个通道均与MSHR阵列相连，用于请求的后续处理。

第二部分是请求缓冲-调度队列。这个队列用于在请求发生阻塞时缓冲请求，并在可以发射时调度等待的请求。通过引入请求缓冲-调度队列，可以允许LLC在重内存负载时接受更多的请求以提升性能。

第三部分是MSHR阵列。MSHR即缺失状态寄存器，在“320 LLC”中用于处理来自CPU的请求。一个MSHR本质上是一个复杂的状态机，通过多个MSHR构建成MSHR阵列，可以使得当前请求正在进行复杂的处理过程时，后续请求仍然可以进入LLC并得到处理，从而实现非阻塞式缓存设计。

第四部分是标签（Tag）与数据阵列。该部分负责储存LLC中数据的标签和数据。为了提升缓存性能，本文采取的是非包含式的设计方案，因此320 LLC除了普通的标签外，还引入了一种只有标签而无对应的数据的标签，称为ExTag。此外，与一般的设计中将标签和数据分别独立划分为两个模块，分开读写不同，在“320 LLC”中这两个模块在逻辑上进行结合，形成了一个两级流水线的形式。所有的读写请求，无论是对标签还是对数据的读写，均需要从同一入口进入。通过将这两个模块绑定，320 LLC减少了内部仲裁的开销，同时减少了竞争可能带来的死锁或者错误。



\section{“320”缓存总线协议设计}

在LLC设计时，需要考虑的第一点因素就是设计其与上级缓存交互的总线接口。缓存总线设计的重要性主要有两点：

第一点是，良好的缓存总线设计可以最大化数据线的利用率。由于物理条件的限制，LLC与上级缓存之间可容纳的数据线是有限的。如果缓存总线设计得冗余，会导致数据通道宽度减小，从而使得需要传输的数据需要更多的时钟周期才能完成传输，增大了数据延迟，减小了数据带宽。同时，如果缓存的总线设置不合理，造成各通道之间利用率不均衡，则会导致某些通道过度繁忙，而其余的通道空间的情况，使得资源没有得到合理的利用。

第二点是，总线设计定义了缓存模块交互的逻辑，进而使得微结构设计中需要分配响应的资源以供总线接口使用。更优化的总线设计可以降低微结构设计中的冗余资源消耗，从而使得整体的片上资源利用率更高。


\subsection{总线数据通道数量的问题}

在设计总线协议时，首先要考虑的是设计哪些通道。总线的通道按照负载类型一般可以分为信息和数据两类。其中信息类型的通道一般只携带地址、类型等负载，开销相对较小。而数据类型的通道要尽可能的宽以高效地传输数据，因而其开销较大。

因此，在设计缓存总线协议时，首先需要考虑的是要设计多少条数据通道。

表\ref{tab:common_cache_bus}给出了以ACE协议\citep{ARM2021}（改进版的AXI协议）、TileLink\citep{TileLink2020}协议为代表的缓存总线协议的通道定义。从表中可以看出，他们有一个共性是均采用了多条数据通道。

本文将上级缓存，即更靠近CPU侧的缓存定义为主端，而下级缓存，即更靠近内存的缓存定义为从端。ACE中，主端到从端的数据通道有两条，分别是W通道与CD通道，用于传输写请求与探听请求(Snoop)的响应数据。而从端到主端的数据通道只有R通道，用于返回读数据。

而TileLink协议中，其定义的5个通道中有4个通道均包含数据。其中A、C通道是主端向从端发送，而B、D通道的信号则是从端向主端发送。TileLink协议中请求的划分较为复杂，如A通道负责发送缓存的读请求，以及非缓存的读写请求，而缓存的写请求则由C通道负责，而C通道同时又作为探听的响应通道。

% Please add the following required packages to your document preamble:
% \usepackage{booktabs}
% \usepackage{multirow} https://www.tablesgenerator.com/
\begin{table}[!htbp]
    \bicaption{\enspace 常见协议通道列表}{\enspace Common Bus Protocol Channels Table}
    \label{tab:common_cache_bus}
    \centering
    \begin{tabular}{@{}lllll@{}}
    \toprule
    协议名称                      & 通道名称 & 通道作用                                                             & 包含数据 & 发送端 \\ \midrule
    \multirow{8}{*}{ACE}      & AR   & 传递读请求                                                            & -    & M   \\
                              & R    & 返回读数据                                                            & 是    & S   \\
                              & AW   & 传递写请求                                                            & -    & M   \\
                              & W    & 传递写数据                                                            & 是    & M   \\
                              & B    & 写响应，表明写入完成                                                       & -    & S   \\
                              & AC   & 传递探听请求                                                        & -    & S   \\
                              & CR   & 响应探听                                                          & -    & M   \\
                              & CD   & 发送探听数据                                                        & 是    & M   \\ \cmidrule(l){1-5} 
    \multirow{5}{*}{TileLink} & A    & 缓存的读与非缓存读写请求                                                     & 是    & M   \\
                              & B    & 传递探听请求                                                        & 是    & S   \\
                              & C    & 响应探听请求、发出逐出请求                                       & 是    & M   \\
                              & D    & 读写请求响应                                                           & 是    & S   \\
                              & E    & 缓存块请求最终握手                                                        & -    & M   \\ \cmidrule(l){1-5}   
    \end{tabular}
    \end{table}

这类设计了多个数据通道的总线协议会产生两个问题：

第一个问题是，大量的数据通道会产生巨大的开销。在通常的设计中，缓存常常使用512 Bits作为缓存行的大小，在总线宽度设计时，往往采用可整除的通道宽度作为数据通道的宽度，如32 Bits、64 Bits、128 Bits、256 Bits甚至512 Bits。为了提升总线带宽，设计时会在硬件资源约束下尽可能地选择更宽的数据通道宽度。数据通道是整个总线协议中，开销最大的通道，相比之下，传送请求和响应的通道由于只包含少量的必要信息，其开销较小。

在协议中采用大量数据通道的总线协议则会增加整体的硬件开销，因此为了在硬件上实现，会反过来压缩数据通道的宽度。假设有一个总线协议原本只需要2条数据通道，而换成了一个使用4条数据通道的协议后，在总资源不变的情况下，每个数据通道就只能拥有原本一半的位宽，从而使得传输数据所需要的时间翻倍，降低整体性能。

第二个问题是，多通道的设计中会出现通道利用不均衡的情况。以CPU最常见的读写操作为例，一个典型的在主从端之间缓存总线协议上可能会发生的事务如表\ref{tab:common_rw_progress} 所示。

从该表可看出，在一次读/写操作中，就读写操作本身仅会产生一次的数据传输，但是有可能因为缓存需要进行一致性的维护而产生探听操作，而主端在响应探听操作时，有可能携带数据也有可能不携带数据。

% Please add the following required packages to your document preamble:
% \usepackage{booktabs}
% \usepackage{multirow}
\begin{table}[!htbp]
    \bicaption{\enspace 读写操作的典型流程}{\enspace Common Progress of R/W Request}
    \label{tab:common_rw_progress}
    \centering
    % \footnotesize% fontsize
    % \setlength{\tabcolsep}{6pt}% column separation
    % \renewcommand{\arraystretch}{1.2}%row space 
    \begin{tabular}{@{}lllll@{}}
    \toprule
    操作                 & 次序 & 事务        & 携带数据 & 必定发送 \\ \midrule
    \multirow{4}{*}{读} & 1  & 主端发送读请求   & -    & 是    \\
                       & 2  & 从端发送探听 & -    & 否    \\
                       & 3  & 主端响应探听 & 可能   & 否    \\
                       & 4  & 从端响应读请求   & 是    & 是    \\\cmidrule(l){1-5} 
    \multirow{4}{*}{写} & 1  & 主端发送写请求   & 是    & 是    \\
                       & 2  & 从端发送探听 & -    & 否    \\
                       & 3  & 主端响应探听 & 可能   & 否    \\
                       & 4  & 从端发送写完成   & -    & 是    \\ \cmidrule(l){1-5} 
    \end{tabular}
    \end{table}

    根据读写操作中事务流程的特性可以看出，将写数据通道与探听响应通道相分离的设计下，会存在着负载不均衡的情况。如ACE协议中专门为维护一致性协议引入了CD通道，在一致性维护请求数量较少时，CD通道的通道利率率将不如R通道与W通道。这就导致了部分带宽资源的浪费。
    
    因此，为了减少总线开销，增加总线带宽利用率，在设计总线协议时需要减少总线中的数据通道数量。

\subsection{总线写通道缓冲区的问题}

由于片上网络在数据传输过程中可能存在延迟、阻塞等问题，因此对于缓存而言，为了保证高性能，缓存会希望尽可能多地将请求缓冲至本地，哪怕暂时该请求不能被处理，否则一个请求处理完毕后再接受另一个请求会产生巨大的延迟。

在缓冲请求时，对于只有请求信息而无数据的请求而言，如读请求，其缓冲所需的成本相对较低。但是如果请求中还携带着数据，那么需要大量昂贵的片上缓冲区才能缓冲这些请求。而在传统的总线设计中，写数据是与写请求同时传递的。如ACE总线协议中，写请求和写数据会同时通过AW和W通道发送。TileLink总线协议中，写通道上同时包含了写请求信息以及数据。这使得缓存必须要设计专用的高开销的额外写缓冲区。

例如在基于TileLink协议的“香山”处理器中，其缓存需要额外地在通道上实现一个”PutBuffer”的结构，该结构负责将写请求附带的写数据暂时缓冲，并将其分配一个索引项，在MSHR分配时，会告知MSHR该索引项，使得其可以从正确的地方读取数据。

这样的额外写缓冲区实际上是专为写请求服务的，读请求无法享受到其带来的好处。而应用负载之间的读写实际上是十分不均衡的，有的程序如视频播放、文件校验，其特点是大量的读与少量的写，而有的程序会产生大量的记录信息，会产生大量的写。因此对写缓冲区而言，不可能存在一个对所有程序都合适的大小。总是会出现缓冲区过大，占用了其他模块的空间，或者缓冲区过小，造成阻塞的问题。

同时，在现代处理器常见的设计中，在上级缓存试图往下级缓存写数据时，会将写请求放入自身的WriteBuffer中，也就是说上级缓存自身也会存在着存放写数据的空间，造成一份数据在两个地方重复存放，产生浪费。

因此，如果能找到办法消除写数据的缓冲区，就可以提升片上资源利用率，增加MSHR等处理模块的数量，增强整体性能。

\subsection{“320”总线}
为了解决以上两点问题，在“320 LLC”中采用的是专门设计的“320”总线。该总线的通道定义见表\ref{tab:req_rsp_bus}，各通道内的信号定义见附表\ref{apptab:1}至附表\ref{apptab:4}，该总线协议的设计受到了CHI协议与OCP协议的启发。从通道设计中可以看出，该“320”总线协议有几点显著的特点与优势。
% Please add the following required packages to your document preamble:
% \usepackage{booktabs}
\begin{table}[!htbp]
    \bicaption{\enspace “320”总线}{\enspace 320 Cache Bus}
    \label{tab:req_rsp_bus}
    \centering
    % \footnotesize% fontsize
    % \setlength{\tabcolsep}{4pt}% column separation
    % \renewcommand{\arraystretch}{1.2}%row space 
    \begin{tabular}{@{}lllll@{}}
        \toprule
        \multicolumn{1}{c}{\textbf{通道名称}} & \multicolumn{1}{c}{\textbf{通道作用}}                                   & \multicolumn{1}{c}{\textbf{含数据}} & \multicolumn{1}{c}{\textbf{\begin{tabular}[c]{@{}c@{}}消息通道\\ 可扩展\end{tabular}}} & \multicolumn{1}{c}{\textbf{\begin{tabular}[c]{@{}c@{}}全通道\\ 可扩展\end{tabular}}} \\ \midrule
        NewReq                            & 上级缓存发送新的读/写请求                                                       & -                                & -                                                                               & 是                                                                              \\
        SnpReq                            & LLC向上级发送探听请求                                                     & -                                & -                                                                               & 是                                                                              \\
        SnpRsp                            & \begin{tabular}[c]{@{}l@{}}上级缓存发送探听应答、\\ 读完成应答、写数据。\end{tabular} & 是                                & 是                                                                               & 是                                                                              \\
        LLCRsp                            & LLC发送读响应、写允许。                                                       & 是                                & 是                                                                               & 是                                                                              \\ \bottomrule
        \end{tabular}
    \end{table}

第一点是：通道以请求与响应作为类型划分。所有的Req通道中的信息均为请求类型，所有Rsp通道中的信息均为响应类型。这样的设计使得模块可以集中地处理总线请求，并转换为内部请求。在缓存的设计中，一般需要将总线上的请求统一转换为内部的请求，然后才能交由MSHR等模块进行处理。而ACE和TileLink协议中，请求可能来自不同的通道，于是转换模块需要同时处理多个不同通道的请求，增加了设计复杂度。

第二点是：在最简设计下，总线的对上以及对下的数据通道各只有一条。已经到达了理论上最少的数据通道数量，使得每个数据通道都可达到最大的带宽和最大的利用率。也避免了多个通道中数据负载不均匀的情况。

第三点是：可进行通道的扩展。当片上资源较为宽裕时，为了提升处理性能，可以对通道数量进行扩展，以获得更高的消息吞吐率。例如可以实现两个NewReq通道，以增强缓存接受新请求的能力。其中，Rsp型通道上还可以进行仅包含消息通道的扩展。由于不是所有的应答消息均包含数据，因此可以扩展出不支持数据传输的Rsp通道，以减少在该类消息传递时的通道占用。

第四点是：所有的总线的数据传输均作为应答进行，无论是主动或被动地进行数据传输，在事务逻辑上均作为响应而发送。

“320”总线解决了以往传统总线协议的两大问题。

首先，在该总线协议中，对上对下的的数据通道各只有一条，这最大化地利用了片上资源，不会出现传统总线协议的数据通道过多，造成通道位宽小，通道负载不均匀的情况。

其次，在该总线协议中，彻底地消除了写数据缓冲区的需要。

\begin{figure}[H]
    \centering
    \includegraphics[width=0.40\textwidth]{write_buf}
    \bicaption{\enspace 传统总线协议中的写缓冲区}{\enspace Write Data Buffer}
    \label{fig:write_buf}
\end{figure}

在传统的总线协议中，写数据往往和写请求一同传输。这就造成了在写请求真正能被处理前，就已经发送出来。如图\ref{fig:write_buf}所示，缓存为了提升Outstanding请求的处理数量，必须要引入写缓冲区将这部分的写数据缓存起来。造成了额外写数据缓冲区的开销。

\begin{figure}[H]
    \centering
    \includegraphics[width=0.40\textwidth]{no_write_buf}
    \bicaption{\enspace ”320”总线消除了写缓冲区的需求}{\enspace No Write Buffer in 320 Cache Bus}
    \label{fig:no_write_buf}
\end{figure}

而在“320”总线设计中，写请求的处理流程有所不同。当写请求发射时，其仅携带需要写入的地址等必要信息，并不携带真正要写入的数据。当LLC真正开始处理该请求后，会返回一个写允许信号，该信号发出后，上级缓存才会真正地将数据发送，此时不会存在阻塞的情况。从而彻底地消除了写缓冲区的需求。
如图\ref{fig:no_write_buf}所示，在整个请求的处理过程中，不需要写缓冲区也可以正常执行。



\section{请求缓冲-调度队列}\label{section_req_array}
在缓存工作时，会由于需要处理重填，一致性维护等原因而产生阻塞。因此常见的做法是引入缺失状态寄存器（MSHR）阵列，将未命中或者需要进一步处理的请求在MSHR阵列中分配一项进行处理，这样一来就不必阻塞主流水线。

但是仅在缓存中引入MSHR阵列还不足以应对所有的阻塞情况。例如，请求A与请求B均在请求相同的物理地址数据，此时这两个请求只能串行处理，否则会出现数据互相覆盖的错误。在一个仅有MSHR阵列的缓存中，遇到这种情况时会造成整个主流水线的阻塞。这类阻塞被称为请求冲突阻塞。另一种情况是，在内存访问压力负载巨大时，所有的MSHR均会被占用，此时新的请求无法进入，这类阻塞称为资源不足阻塞。

因此，为解决请求冲突阻塞与资源不足阻塞，需要引入一个额外的缓冲队列来将这类请求暂存，同时还需要一个调度器使得冲突结束时尽快将其调度并发射至MSHR。这样的队列在本文中称为请求缓冲-调度队列。
请求缓冲-调度队列与MSHR阵列相比，其最大的特点是没有直接的请求处理能力，所有的请求最终仍然需要交付给MSHR进行处理。因此，在设计时，有几个需要重点考虑的点：

首先，缓冲-调度队列的实现必须能在较小的硬件开销下提供较大的容量。因为本质上缓冲-调度队列是一个较弱功能的缓冲区，没有任何处理请求的功能，是作为MSHR的后备缓冲使用的。如果实现开销较大，不如用相同的资源去放置更多的MSHR达到更高的处理效率。

其次，缓冲-调度队列中，调度算法的实现必须要能以较低的成本扩展，在考虑高效性的同时，需要避免死锁问题。因为在重内存压力负载下，冲突几乎每时每刻都存在，因此调度算法首先要能高效地调度待发射的请求以提升性能，同时也要避免存在一些请求在每次尝试调度时均被阻塞，产生“饿死”的问题，否则会造成CPU流水线卡死。

\subsection{相关项目中请求缓冲-调度队列存在的问题}
在ChipYard项目的缓存中，其缓存-调度队列的实现是一个链表阵列，如图\ref{fig:chipyard_reqarray}所示。具体而言，它为每个MSHR维护了一个链表头，当一个新请求被阻塞后，会在链表节点池中分配一个节点，并挂在一个MSHR的链表后面。这样的设计理论上来说可以使得每个MSHR都独享一个请求暂存队列，同时共享缓冲区资源。
\begin{figure}[!htbp]
    \centering
    \includegraphics[width=1\textwidth]{chipyard_reqarray}
    \bicaption{\enspace ChipYard中的链表式请求缓冲-调度队列}{\enspace Req Array in ChipYard}
    \label{fig:chipyard_reqarray}
\end{figure}

但是在实际硬件中，链表式请求缓冲-调度队列是不可实现的。因为链表本质上是一种多级间接寻址的结构，以MSHR处理一个新请求为例，首先MSHR需要访问Head阵列中对应的元素，接着通过Head访问链表节点池中对应的数据。接着，还需要将该节点的Next指针读出并替换到Head指针中。而插入一个新元素的步骤更为繁琐，首先新请求需要在链表节点池中分配一项，接着找到对应MSHR的Tail指针，接着通过Tail指针索引到末位元素并修改末尾元素的Next指针，最后修改Tail指针。也就是说，每次操作请求需要进行不只一次的间接索引，极大地增加了硬件延迟，甚至在单独的这一个链表模块就会产生时序违例。
\begin{figure}[!htbp]
    \centering
    \includegraphics[width=0.5\textwidth]{xiangshan_reqarray}
    \bicaption{\enspace "香山"中的单缓冲式请求缓冲-调度队列}{\enspace Req Array in XiangShan}
    \label{fig:xiangshan_reqarray}
\end{figure}

而在在“香山”项目中，他们为了规避链表式请求缓冲-调度队列产生的高延迟问题，采用的方式是简单地为每一项MSHR实现一个单项的缓冲区。这样的设计成功消除了链表式缓冲区高开销的问题，设计可在真实硬件上实现，但是其缓冲的项数太少，导致十分容易出现整体阻塞。



\subsection{多级计数请求缓冲-调度队列}

\begin{figure}[!htbp]
    \centering
    \includegraphics[width=1\textwidth]{320_reqarray}
    \bicaption{\enspace "320 LLC"中的多级计数请求缓冲-调度队列}{\enspace Multi-Threshold Req Array in 320 LLC}
    \label{fig:320_reqarray}
\end{figure}

ChipYard和“香山”的请求缓冲-调度队列设计暗含着一个假设，那就是所有的请求必须有序地进行处理。如ChipYard的设计中，他们使用链表并且使用尾插法插入新节点，同时规定每个MSHR只处理一个组，是为了保证同组的所有请求按序处理。而香山也规定每个MSHR只能处理一个组，为了缩减硬件开销而只容许单个缓冲区。

但实际上，在请求从CPU侧传输到LLC的过程中，绝大多数的总线协议本身并不保证传输顺序，例如ACE协议中并没有对同一通道上的传输做先后顺序的要求。因此，LLC看到的请求本身就是有可能乱序的。同时，在多核视角来看，除非使用内存屏障指令等操作强制内存进行同步，否则在常见的多核一致性模型下，是不对多核之间访存的顺序进行要求的。

因此，在LLC侧没有必要强加一个顺序执行的约束。为了实现更低成本可扩展的请求-调度队列，可以容许请求乱序执行。“320 LLC”中设计了一个基于轮转调度(Round-Robin,RR)算法的缓冲调度结构。当请求进入后，会被分配一项，然后根据片转调度算法在每周期尝试发射一个请求。这样的缓冲队列由于其结构的简单性，可以较好地扩展，并且开销较少，可以于硬件上实现。

但是单纯使用RR算法进行调度会存在问题，比如说在一个重负载的场景下，冲突不断地在产生，而单纯的轮转调度算法会周期性地尝试调度每个请求。有一种可能是，某一个请求A在每次试图调度时，均受到阻塞，而阻塞消失后，调度器调度了另一个请求B，使得请求A永远地受到阻塞，产生“饿死”的情况。

为了解决饿死问题，“320 LLC”在缓冲队列中的每一项均引入了一个计数器，计数器反映了尝试发射失败的次数。当计数器达到上限时，则说明这个请求已经长时间未能发射，此时RR算法将会暂停轮转，反复尝试发射该请求直到该请求被成功发射为止。

但是单纯引入计数器以及计数器上限还会存在一个问题，那就是单一的计数值上限无法满足不同情况下的需求。例如，当缓冲区内只有两个请求时，计数器的值会快速上涨从而瞬间达到阈值，从而使得后续的请求均被阻塞，而此时实际经过的时间并不长，远未到饥饿的水平。如果加大计数器上限，当缓冲区内存在着较多请求时，等待计数器达到阈值所需的时间会大幅度增加。

因此，本文在缓冲队列中引入了多级计数器上限的机制。计数器上限根据当前缓冲区内的请求个数动态调整。当缓冲区内的请求个数较少时，计数器的上限会取一个较大的值。而缓冲区的请求个数减少时，计数器的上限会相应地减少。通过该优化，320 LLC在SPEC 2006的Libquantum等子项的性能得到了提升。

\section{非包含式缓存设计}

决定缓存性能的两大因素分别是延迟以及命中率。其中，命中率又往往与缓存容量正相关。由于实现的简单性，许多项目中的缓存均采用的是包含式设计，如ChipYard，香山等。在包含式缓存的设计中，有大量的数据会同时存在于多级的缓存层次结构中，因此导致缓存的容量浪费。而排它式缓存由于需要严格维护数据的非包含性，因此会产生额外的开销。因此，为了达到更高的性能，在“320 LLC”中运用的是非包含式的缓存设计，该设计允许数据只存在于上级缓存而不存在于下级缓存中，但是不必要严格地保证这一点。

\subsection{基于过滤器的非包含式缓存设计的问题}

在包含式缓存设计中，所有的上级缓存中存在的数据必然能在末级缓存的元数据，也称为目录，内查询到。因此缓存的目录天生就可充当一个完美的探听过滤器（Snoop Filter）。在非包含式缓存的设计中，上级缓存存在的数据不一定在下级缓存中存在，而末级缓存在维护一致性时，需要知晓缓存数据在各CPU核中的存放情况。这就引出了一个难题，那就是如何追踪在上级缓存而不在末级缓存中的数据状态。

最基础的做法是在每次需要进行一致性维护时，向所有的CPU发送探听信号（Snoop），但是这会造成总线上存在大量的探听请求，挤占总线的带宽。因此设计人员会在中引入探听过滤器以减少不必要的探听请求\citep{Salapura_Blumrich_Gara_2008}\citep{conway2010cache}。以最常用的过滤器Bloom Filter\citep{Bloom_1970}为例，它可以保证不会存在假阴性（False Negative），但是可能存在假阳性（False Positive），即它有可能误报某个元素存在集合中，但是不会误报某个元素不在该集合中。

基于过滤器的实现方法的特点是节省空间，可以有效地过滤掉很多无用的探听请求。但是过滤器自身也存在着一些问题，首先是过滤器是单向过滤的，因此往往需要多个不同方向的过滤器协同工作\citep{Salapura_Blumrich_Gara_2008}，如一个过滤器负责记录在CPU中的地址，另一个过滤器负责记录不在CPU中的地址。其次，当过滤器不断使用时，其过滤能力会逐渐下降，如Bloom Filter过度使用后，会退化地认为所有地址都可能存在于集合中。此时需要周期性的清空过滤器\citep{Moshovos_Memik_Falsafi_Choudhary_2002}\citep{Salapura_Blumrich_Gara_2007}，而清空过滤器会短暂地使得系统恢复完全没有过滤的情况，因此还需要使用半刷新的方式，在新的过滤器过滤效果上升前暂时使用旧过滤器的信息，但是禁止更新过滤器。这些问题都使得系统变得更加复杂。

\subsection{目录包含-数据非包含式缓存设计}
受到前人工作的启发\citep{Zhao_Iyer_Makineni_Newell_Cheng_2010}\citep{Yan_Sprabery_Gopireddy_Fletcher_Campbell_Torrellas_2019}，“320 LLC”中采取的是目录包含-数据非包含的设计方案。在缓存中，一个缓存行的数据占用64Byte的空间，而标签（包括其他状态信息）所占空间不到8B，扩展标签的代价相对较低。因此，可以扩展目录从而使得目录可以追踪更多的数据，但是同时保持数据存储部分不变。使得可以同时获得非包含缓存更大等效容量的好处，在处理一致性协议的时候又可以获得传统包含式缓存的易于管理的好处。

具体而言，“320 LLC”中引入了一类新的标签，称为ExTag，这类标签的特点是只有标签而没有数据，专门用于追踪不存在于末级缓存而存在于上级缓存的数据。而在传统的缓存中，标签与数据是一一对应的关系，也就是说其只能追踪存在于末级缓存中的数据，而无法追踪只存在于上级缓存的数据。


\begin{figure}[!htbp]
    \centering
    \includegraphics[width=0.8\textwidth]{ex_tag}
    \bicaption{\enspace ExTag示意图}{\enspace ExTag schematic diagram}
    \label{fig:ex_tag}
\end{figure}

在非包含式的缓存设计中，一大关键点是如何设计数据插入、替换等事件下的数据处理策略。

当新请求到来，LLC试图分配新行时，首先会检查是否有空余的ExTag，如果存在空余的ExTag，那么会将新行分配至ExTag，并将数据直接传递给上级缓存，此时的数据处于非包含式的状态。如果ExTag均已满，那么此时会选择在普通标签中插入一行，此时数据保持为包含状态。

在上级缓存发出逐出请求时，LLC会查询该请求对应地址处在的位置。若该请求对应的是普通标签，则按照一般的包含式缓存处理方法进行，更新Data以及更新状态。如果是处于ExTag，那么此时会将ExTag对应的路清空，然后选择普通标签中的一路进行插入。此时的LLC表现得像是上级缓存的受害者缓存(Victim Cache)。值得需要注意的是，处于非包含式状态的数据，在从上级缓存逐出时，即便是处于干净状态，也需要将数据写回。因为此时如果不将数据写回，相当于丢弃了数据，而相比写回的开销而言，重新从内存中取回数据的开销要显著更大

假设客户A持有了数据X，并处于非包含式状态时，此时如果客户B也试图获取数据X。此时数据将直接通过客户A取得，并于ExTag内更新相关的记录信息，而不会将数据先保存至LLC，再转发至客户B。这样做可以避免移动过程中产生的额外一致性协议维护开销。

\section{同组请求并行设计}
\subsection{同组请求并行可能产生的问题}
在缓存的访问过程中，通常会经历这几个步骤：首先，从物理地址中提取若干位，将这几位当作组索引。接着，用组索引去读取标签阵列，得到若干路的结果。然后，比对所有路中的标签与原始请求地址的标签，得知是否发生了缓存命中。如果缓存命中，则将结果返回，如果不命中，那么需要用替换算法进行路分配，接着将重填的数据填入到被分配的路中。

当两个请求的物理地址有相同的组索引时，就称为发生了组冲突。如果希望缓存能高效的处理请求，那么需要实现同组请求并行处理，然而同组请求并行处理可能产生错误执行的情况。

\begin{figure}[!htbp]
    \centering
    \includegraphics[width=0.8\textwidth]{set_parral_error}
    \bicaption{\enspace 同组请求并行执行导致错误}{\enspace Set Parallel Execution Error}
    \label{fig:set_parral_error}
\end{figure}


如图\ref{fig:set_parral_error}所示，假设Req\#0与Req\#1是两个不同地址的请求，但是拥有相同的组。这两个请求在进入LLC后，发现均未命中，因此各自需要分配一路进行重填。但是此时路分配算法在两次分配中，均分配到了同一路Way\#1，接着两个MSHR在并行地处理这两个请求，请求处理完成后，将更新完的数据写回了Way\#1。此时就出现了两个地址记录数据互相覆盖的情况，会导致整体信息维护错误。因此，一个最简单避免该错误的方法就是，禁止同组的请求并行执行，ChipYard与“香山”的缓存均采用了串行执行的策略。这个设计方案虽然较为简单，但是在重内存负载的场景下，发生组冲突的概率增加，此时同组串行执行会导致性能损失。


\subsection{同组请求并行执行的设计}

为了提升重负载下的缓存性能，在该项目中加入了同组并行执行的设计。同组并行有可能产生错误的原因是替换算法有可能出现的路重复分配，那么只要能确保不会出现路重复分配的情况即可能正确地实现同组并行执行。

\begin{figure}[!htbp]
    \centering
    \includegraphics[width=0.8\textwidth]{set_parral_320}
    \bicaption{\enspace 同组请求并行执行解决方案}{\enspace Set Parallel Execution}
    \label{fig:set_parral_320}
\end{figure}

从请求的生命周期来看，请求的执行可以分为几个部分。第一个部分是请求到达请求缓冲队列，然后试图发射。第二部分是请求被发射，然后读取标签、目录等信息，分配待插入的路号。第三部分是进入了MSHR进入后续处理流程。为了保证不会出现路重复分配，在第一部分请求即将发射时，要收集目前所有正在处理的组冲突的请求分配的路号，接着将已使用路的位向量携带至替换算法处并告知替换算法不要从已分配的路号中进行分配。但这一方法存在问题，那就是在同组冲突的请求进入到了第二部分，但是尚未分配到路号时，此时无法确定其即将占用的路，如果要实现对这一部分的冲突路号判断，需要复杂的转发与判断逻辑，这会使得请求发射的逻辑复杂度极度上升，有时序违例的风险。

因此，本文中采用的是一种折中的方案，将第二部分划分为“危险区”。当发现有组冲突的请求在“危险区”时，则会暂时退化成同组串行，当第二部分内不再存在同组冲突的请求时，再恢复为同组并行。这样的选择使得性能和硬件复杂度得到了一个较好的平衡。

同时，在同组并行执行的框架下，对替换算法的更新步骤也需要进行改动。在传统的同组串行执行的框架中，每次替换算法在更新的时候，只会更新一路对应的数据。而同组并行执行的情况下，相当于有多个路会同时访问，因此替换算法在更新时，还需要同时更新当前已占用的路对应的数据。

\section{一致性与死锁的处理}

在LLC中，一个很重要的功能就是要能正确地处理数据的一致性，同时不能出现死锁问题。一致性与死锁这两个问题往往是相伴出现的，出现需要维护一致性的地方通常是由于有多个模块或者请求在相互竞争，而维护一致性本身又会产生新的请求，如果处理不当就容易产生死锁。

从一致性的角度来看，一致性分为外部的一致性与内部的一致性。外部的一致性即是缓存一致性，上层的多个缓存之间数据需要保持一致。内部的一致性是指各MSHR之间进行数据读写时需要保持的一致。

从死锁的角度上来说，有两种主要的死锁类型需要避免。第一种需要避免的死锁类型是协议级的死锁，即在总线协议的设计的逻辑上是要保证无死锁的，在上下级缓存的各个部件正常工作时，总线协议不应该产生死锁。第二种需要避免的，是硬件结构上的死锁，即各部件在竞争读写端口等资源的时候，需要合理调配，不能发生死锁的情况。

\subsection{缓存一致性协议}

在“320 LLC”中为了维护多个上级缓存之间数据的一致性，采用的是一种特殊的MOESI缓存一致性协议。MOESI是上级缓存可处于的5种状态的简写，这五种状态分别是：

已修改(Modified)：这个状态表示着该缓存拥有缓存数据的唯一副本。并且已经修改过，即处于“脏”状态。该缓存后续可能进一步地修改该数据。当缓存不再想持有该数据时，必须将其写回下级缓存，即LLC。

持有(Owned)：这个状态代表着该缓存正持有一个下级缓存没有的数据副本。在缓存不再想持有该数据时，必须将该数据写回下级缓存，无论该数据是否为脏。

独占(Exclusive)：这个状态代表着该缓存具有该数据的唯一副本，该副本目前处于干净的状态，该缓存可以自由地修改该数据。当数据写入后，会转为Modified状态。

共享(Share)：这个状态代表着该缓存拥有着该数据的只读副本，这可能是系统中的多个缓存副本之一。处于Share状态的缓存行不能直接被修改，如果需要修改，小，需要先转换为Modified状态。处于共享状态的缓存行可以直接丢弃而不用通知下级缓存。

无效(Invalid)：这个状态代表着该缓存没有持有该数据的任何副本。缓存必须先获取该数据才能进行进一步的读写操作。

MOESI协议对于数据一致性的保障是基于一个简单的观察，即只读状态下不会产生一致性错误，无论多少个缓存同时持有副本，其数据均是一致的。而如果数据处于可读写的状态，同时此时多个缓存都持有该副本，当一个缓存修改了数据后，就会使得该缓存的副本与其他缓存的副本出现不一致的情况。

因此，MOESI协议中规定如果一个数据处于可写的状态，那么只能最多有一个缓存正在持有该可写副本，如已修改和独占状态。同时，在上下级缓存之间也需要维护一致性，因此MOESI协议规定了，当上下级缓存数据不一致时，上级缓存在释放数据的过程中需要将数据写回下级缓存，如持有和已修改状态。

MOESI协议中容许的缓存状态对见表\ref{tab:MOESI_PERMITED_STATE}。


% Please add the following required packages to your document preamble:
% \usepackage[table,xcdraw]{xcolor}
% Beamer presentation requires \usepackage{colortbl} instead of \usepackage[table,xcdraw]{xcolor}
\begin{table}[!htbp]
    \bicaption{\enspace MOESI协议容许的缓存状态对}{\enspace Permitted Cache States By MOESI}
    \label{tab:MOESI_PERMITED_STATE}
    \centering
    \footnotesize% fontsize
    \setlength{\tabcolsep}{4pt}% column separation
    \renewcommand{\arraystretch}{1.2}%row space 
    \begin{tabular}{
    >{\columncolor[HTML]{EAECF0}}c 
    >{\columncolor[HTML]{F8F9FA}}c 
    >{\columncolor[HTML]{F8F9FA}}c 
    >{\columncolor[HTML]{F8F9FA}}c 
    >{\columncolor[HTML]{F8F9FA}}c 
    >{\columncolor[HTML]{F8F9FA}}c }
    {\color[HTML]{202122} \textbf{}} &
      \cellcolor[HTML]{EAECF0}{\color[HTML]{202122} \textbf{M}} &
      \cellcolor[HTML]{EAECF0}{\color[HTML]{202122} \textbf{O}} &
      \cellcolor[HTML]{EAECF0}{\color[HTML]{202122} \textbf{E}} &
      \cellcolor[HTML]{EAECF0}{\color[HTML]{202122} \textbf{S}} &
      \cellcolor[HTML]{EAECF0}{\color[HTML]{202122} \textbf{I}} \\
    {\color[HTML]{202122} \textbf{M}} & {\color[HTML]{202122} N} & {\color[HTML]{202122} N} & {\color[HTML]{202122} N} & {\color[HTML]{202122} N} & {\color[HTML]{202122} Y} \\
    {\color[HTML]{202122} \textbf{O}} & {\color[HTML]{202122} N} & {\color[HTML]{202122} N} & {\color[HTML]{202122} N} & {\color[HTML]{202122} Y} & {\color[HTML]{202122} Y} \\
    {\color[HTML]{202122} \textbf{E}} & {\color[HTML]{202122} N} & {\color[HTML]{202122} N} & {\color[HTML]{202122} N} & {\color[HTML]{202122} N} & {\color[HTML]{202122} Y} \\
    {\color[HTML]{202122} \textbf{S}} & {\color[HTML]{202122} N} & {\color[HTML]{202122} Y} & {\color[HTML]{202122} N} & {\color[HTML]{202122} Y} & {\color[HTML]{202122} Y} \\
    {\color[HTML]{202122} \textbf{I}} & {\color[HTML]{202122} Y} & {\color[HTML]{202122} Y} & {\color[HTML]{202122} Y} & {\color[HTML]{202122} Y} & {\color[HTML]{202122} Y}
    \end{tabular}
    \end{table}

在ARMv8架构使用的MOESI协议中，持有状态指的是一个缓存最初拥有脏块，处于已修改状态，接着另一缓存试图读取这一缓存块，此时脏块并未写回下级缓存，而是转换成了共享的脏状态（Share Dirty），称为持有状态。

而“320 LLC”中使用的MOESI协议中，持有状态指的是处于非包含状态的共享的缓存块，此时缓存持有的数据块仅存在于上级缓存中而不存在于下级缓存中。因此持有状态的缓存无论是否为脏，当释放数据时均需要写回。

“320 LLC”的MOESI协议状态转移图如图\ref{fig:MOESI_TRANS}所示。

\begin{figure}[!htbp]
    \centering
    \includegraphics[width=1\textwidth]{MOESI_TRANS}
    \bicaption{\enspace MOESI协议的状态转移图}{\enspace MOESI State Transition}
    \label{fig:MOESI_TRANS}
\end{figure}


\subsection{一致性维护边界情况处理}

MOESI协议只是在理论上阐述了在各种访问情况下上级缓存的状态以及状态转移关系。但是在实际实现中，缓存的状态转移并不是瞬间完成的，上级缓存需要和下级缓存之间发送各种类型的请求以完成数据传输以及信息沟通的操作。

缓存之间的消息传递，基本都存在着“请求-应答”这样的两个阶段。也就是说，当上级缓存发出一个新的读请求或写请求时，该请求不能立刻完成，而需要等待下级缓存给出对应的响应后，才能认为请求彻底完成。同时，消息在总线上的传输可能存在延迟，总线本身可能会存在阻塞的情况，因此就造成了上级缓存的内部状态与下级缓存所认为的状态不一致的情况。

如果在一段时间内，对同一数据块的访问仅有一个上级缓存在进行，那么短暂的上下级缓存状态不一致不会产生问题，因为当缓存请求完成之后，上下级缓存中记录的状态会恢复一致。如果同一时间内，有多个请求都在请求访问同一数据块，但是请求之间不会互相影响对方的状态，如两个请求都是获取Share状态的数据块，那么两个请求不会干扰对方的状态，因此也不会产生问题。

真正需要重点处理的是在一个请求未完成时，另一个请求又到来，且后续请求会对前一请求状态产生修改的情况。由于LLC在对某一上级缓存的状态进行修改时，需要发送探听请求以进行一致性维护操作，因此在处理一致性维护的边界情况时，不需要在多个上级缓存的视角进行处理，只需要在单独的一个缓存中正确地处理请求与探听请求的关系即可。


\textbf{上级缓存读请求处理完成前，收到了探听请求时的处理}

从下级缓存（即LLC）的视角来看，出现这种现象有两种可能的情况。第一种情况是，上级缓存的读请求在等待处理中，还没有得到处理。但是另一请求，或者由于LLC自身的替换原因，导致需要向上级缓存发送探听。第二种情况是，LLC已经处理了上级缓存的读请求，并且返回了读响应，但是后续又发出了探听，由于总线上存在阻塞等原因，导致后发的探听比先发的读响应先到。

在第一种情况下，本质上该请求未处理，因此应当视同该读请求未发出，需要让上级缓存能够正常地响应探听消息，并按照请求发出前的状态进行探听的响应。当LLC继续处理到此前被阻塞的读请求时，按照正常的读请求流程处理。

而第二种情况会使得一致性维护十分困难，因为对上级缓存而言，其并不知道在收到应答前收到的探听处于哪一种情况。因此上级缓存会期待着处理探听之后会继续收到一个读响应，因此，最终上级缓存的状态会变成持有该数据的状态。而从下级缓存的角度来看，此时上级缓存已经响应了后发的探听，其状态应该处于无效状态，因此出现了不一致的情况。

在“320 LLC”中采取的方案是，通过施加限制，使得第二种情况不会出现。在LLC处理了一个读请求后，必须确保上级缓存成功接收到读响应后才能发出后续的探听请求。但是总线的阻塞和传输情况无法预测，因此要求上级缓存收到了读请求之后，向下级缓存发送一个读应答，当LLC收到该读应答后，便可确定上级缓存已经完成了读操作。

\textbf{上级缓存写请求处理完成前，收到了探听请求时的处理}

写请求可能产生问题的原因与读请求类似，因此也可以用类似的方法来解决该冲突。对于上级缓存而言，在写操作完成之前，都要正常地视为自身持有数据以响应探听请求。对于上级缓存而言，写操作完成的标志是下级缓存发送写允许信号，接着自身发送写数据，协议保证在写允许信号与写数据之间不会存在任何探听信号。对于下级缓存而言，在发送写允许信号前，可以自由地发送探听，但是一但开始处理写请求，在发送了写允许信号后，必须等待写数据到来才能继续发送探听。

写请求与读请求在处理过程中存在着一些差异，当写回请求已发送但是未处理时，此时上级缓存的状态是包含了待写回的脏数据。而经过了探听后，上级缓存的状态变为了无效，即不持有任何数据。但是当探听后，LLC继续处理先前的请求，会发现一个无效状态的缓存块试图写回。因此在该情况下，上级缓存必须要在写应答到来后，发送一个取消写回操作以取消该请求，否则会导致无效数据被写到LLC中。


\subsection{一致性维护中的死锁避免}

缓存的请求与一致性维护操作都是由MSHR进行处理，本质上来说，缓存之间的信息交互就是上下级缓存MSHR的交互过程。由于在收发缓存请求时，上下级缓存均需要分配MSHR来进行，因此当MSHR资源不足时，就会导致缓存无法再接受新的请求，而上下级MSHR资源同时不足时，就会发生死锁问题。

之所以上下级MSHR资源同时不足时会发生死锁，是因为在处理缓存请求时，通常要进行一致性维护的操作。如处理读请求时，下级缓存可能会发出一个新的探听请求来进行一致性维护，此时需要上级缓存有MSHR资源来响应这一个新的请求。

因此，通常的解决方案是根据总线协议中各信号的优先级关系，在缓存中为高优先级的请求预留出专用的MSHR。如“香山”项目中为通道B与通道C分别预留了一个MSHR。

在“320 LLC”使用的“320”总线中，读写请求均为同一优先级，而探听请求具有更高的优先级。因此，在“320”总线中规定，上级缓存必须要预留出一个特殊的MSHR，这个MSHR只允许用于响应探听请求，而不允许主动发出读写请求。通过这样的规定，可以避免发生死锁的情况。


\subsection{缓存微架构中的死锁避免}

在LLC内部的微架构中，同样存在着产生死锁的可能，需要加以避免。第一个可能出现死锁的地方是请求缓冲-调度队列内，这部分的死锁避免已经在\ref{section_req_array}章节进行介绍，此处不再赘述。而第二个可能产生死锁的地方是在标签阵列和数据阵列的读写部分。

由于标签阵列和数据阵列容量较大，使用多端口进行读写的硬件开销较大，因此往往只能采用单端口的硬件实现。这就导致了读写端口资源是有限的。在设计时必须要能恰当地调度各类对读写端口的请求，否则可能产生死锁。

在标签阵列的读写端口调度处理中相对简单，因为此时尝试进行读写标签阵列的请求来源只有两种：

\begin{enumerate}
    \item 新请求的读标签请求
    \item MSHR处理完成后更新标签的请求
\end{enumerate}

此时LLC需要将MSHR的请求置于一个更高的优先级，以此保证MSHR可以及时地完成写回并得到释放，否则可能出现新请求堵死了标签端口的情况。这会造成导致所有MSHR均无法释放，进而导致MSHR资源不足，LLC死锁。

但是数据阵列的读写请求调度就更为复杂，因为可能读写数据阵列的来源更多：

\begin{enumerate}
    \item 命中读请求读取数据
    \item 写请求写回数据
    \item 探听响应写回数据
    \item 未命中的读请求重填数据
\end{enumerate}

在ChipYard项目中，其缓存采用的是TileLink缓存总线协议，因此这几种数据阵列的读写请求都拥有不同的优先级，他们采用的解决方式是按照不同的优先级来依次调度等待中的请求。但是这种方式会在访问数据前加上一个延迟较大的带优先级的多选一结构，使得本就延迟较大的数据访问更是雪上加霜。

而“320 LLC”采用的是“320”总线。该总线协议中并不区分读写请求的优先级。因此可以将缓存读写请求合并。同时，未命中的请求进行重填、探听响应均会回到MSHR中，因此可以将这两种请求均合并至MSHR更新数据时处理。此时，四种不同的数据读写来源就被压缩成只剩两类：

\begin{enumerate}
    \item 新请求的读写数据请求
    \item MSHR处理完成后更新数据的请求
\end{enumerate}

\begin{figure}[!htbp]
    \centering
    \includegraphics[width=0.7\textwidth]{320_tag_and_data}
    \bicaption{\enspace 合并的标签与数据阵列结构}{\enspace Combinded Tag and Data Array}
    \label{fig:320_tag_and_data}
\end{figure}

可以观察到，这两类数据请求与标签请求高度一致，同时可以运用同样的优先级关系。并且标签和数据的访问往往是同步进行的。因此在“320 LLC”中将标签与数据阵列合并，如图\ref{fig:320_tag_and_data}所示，成为一个二级流水线的结构。这样一来可以将标签和数据的读写请求合并，在同一阶段进行调度，减少多处调度的开销和产生问题的可能性，在调度完成之后数据请求可以缓存一拍再真正发送给数据阵列，这样一来可以达到优化时序的效果。在“320 LLC”中，标签与数据阵列均采用分块（Bank）的设计，在每个块前都需要一个多路选择器，因此减少多路选择器的开销对整体缓存设计而言十分重要。





\section{代码编写与测试}

本工作的目标是研发一个高性能的RTL级的高性能末级缓存，以为其他体系结构研究人员提供一个开发与实验平台。为此需要解决两个问题，第一个是采用什么编程语言进行编程，第二个是代码要如何进行测试。

\subsection{敏捷硬件开发语言}

RTL级代码，即寄存器传输级代码，是对数字电路的抽象描述代码，可以映射到FPGA、ASIC等实际硬件上。一般需要采用HDL语言即硬件描述语言进行书写，传统的HDL语言有verilog、VHDL等。但是这些硬件描述语言语法较为死板，表达性较差，编写与修改都较为困难。

为了提升硬件代码的编写效率，近年来出现了一些新型的硬件敏捷开发语言，如SpinalHDL\citep{SpinalHDL}，Chisel\citep{chisel}等，这类语言属于生成式语言，允许编程者先使用高级语言进行开发，然后将这些代码生成为Verilog、VHDL等较为传统的硬件描述语言。如ChipYard与“香山”项目均采用了Chisel作为其开发语言，Chisel语言基于Scala，可以利用Scala的面向对象、函数式编程等先进特性来加速硬件的开发流程。但是Chisel语言在生成Verilog的过程中会擅自优化和改动部分连线，同时对硬件类型和线宽没有检查，造成实际使用中难以调试，给开发人员造成了困难。

因此“320 LLC”采用的是SpinalHDL作为项目的开发语言，这也是一款同样基于Scala的硬件描述语言，同样可以采用面向对象、函数式编程等特性来加速硬件加法流程。但是其拥有更强的类型检查，支持辅助调试信息、生成代码一致性强等优点，因此SpinalHDL更适合需要进行严谨开发的流程。

通过采用SpinalHDL语言，使得开发出来的LLC代码具有高度的参数化以及模块化的特点，可以通过配置文件来快速生成具有不同参数配置的代码，同时其抽象性使得代码复用率提升，简化了开发流程。

\subsection{单元级正确性测试框架}
在硬件开发中，很重要的一环是进行模块级测试以验证其功能的正确性。但是LLC需要从多个上级缓存处接受请求并应答，请求的顺序和应答的顺序往往是不一致的，同时还需要考虑到多个上级缓存之间状态是否能正确维护，给验证带来了很大的挑战。

为此，本文设计了“观察者”LLC验证平台，以用于对LLC进行功能性验证。

\begin{figure}[H]
    \centering
    \includegraphics[width=0.8\textwidth]{llc_monitor}
    \bicaption{\enspace LLC功能验证平台}{\enspace LLC Function Verification Platform}
    \label{fig:llc_monitor}
\end{figure}

“观察者”验证平台由几部分组成。

第一部分是LLC硬件仿真部分，该部分由Verilator仿真器对LLC的代码进行仿真。并通过SpinalSim库与软件控制部分交互。

第二部分是虚拟CPU部分，该部分由Scala代码编写，通过SpinalSim库来与仿真中的LLC代码进行数据交互。这部分代码会启动多个线程以模拟多个CPU，每一个线程都会从一个内存地址池中随机地选出一些地址进行读写操作，并向LLC发送对应的读写请求。每个虚拟CPU的操作之间都有不确定的延迟，以更好地模拟真实情况。

第三部分是监控器部分，监控器分为两种类型，第一种类型是CPU监控器，用于监控每一个虚拟CPU所获得的响应已经状态符合自身预期。第二种类型是LLC总线监控器，通过监控LLC的总线来记录目前LLC所认为的已确定的CPU状态，同时与CPU监控器做比较以检测任何的的潜在错误。

在测试时，“观察者”验证平台通过向LLC发送大量随机访存命令，并实时比对返回的数据、状态是否符合预期，以验证LLC的功能正确性。

\subsection{系统级测试框架}



功能级验证平台只能验证LLC的正确性，但是如果需要验证性能，以及和实际CPU的适配情况，需要将LLC置于真实的CPU框架内进行测试。


为了进行高效处理器微结构的设计空间探索，“320”项目被提出，其使用高级硬件描述语言SpinalHDL进行RTL代码的书写，使用高速开源仿真加速器verilator进行仿真，设计中注重参数化和可替换性。其分为RTL级处理器代码，以及仿真验证开发框架这两大部分，如图\ref{fig:Universe_320}所示。
 
\begin{figure}[H]
    \centering
    \includegraphics[width=0.6\textwidth]{Universe_320}
    \bicaption{\enspace 320平台示意图}{\enspace Diagram of the 320 Platform}
    \label{fig:Universe_320}
\end{figure}


RTL级处理器代码中又可分为320 CPU核与320 LLC。其中320 CPU核部分为ICache、前端、后端、DCache四大模块，采用的是分离式前端、6发射、乱序执行的设计。而320 LLC即为本文所设计的高速末级缓存。RTL级处理器代码的这几个部分之间采用了解耦合的设计，使得更换特定的部分不会影响其他部分的代码，方便进行设计空间的探索。

而仿真验证框架的部分由多种激励生成与测试模块组成，可以支持对处理器代码进行三种类型的仿真，第一种是进行功能验证的随机指令测试，第二种是Linux内核启动测试，第三种是基于Simpoint的性能测试。



\subsection{实验结果}

\textbf{正确性测试}

本文首先分别使用单元级正确性测试框架，以及系统级测试框架中的正确性测试部分对320 LLC进行了测试。测试结果如表\ref{tab:llc_correctness}所示。

\begin{table}[H]
    \bicaption{\enspace 320 LLC正确性测试结果}{\enspace 320 LLC Correctness Test Results}
    \label{tab:llc_correctness}
    \centering
    \footnotesize% fontsize
    \setlength{\tabcolsep}{4pt}% column separation
    \renewcommand{\arraystretch}{1.2}%row space 
    \begin{tabular}{@{}cc@{}}
    \toprule
    测试项目        & 测试结果                                                          \\ \midrule
    虚拟CPU负载测试   & 通过测试，未报错                                                      \\
    CPU随机指令测试   & 通过测试，未报错                                                      \\
    Linux系统启动测试 & 进入Shell界面，能运行指令                                               \\ \midrule
    随机硬件参数测试    & \begin{tabular}[c]{@{}c@{}}各硬件参数下均通过测试，\\ 未发生死锁\end{tabular}  \\
    地址冲突请求测试    & \begin{tabular}[c]{@{}c@{}}各请求均能正常响应，\\ 未发生死锁或错误\end{tabular} \\ \bottomrule
    \end{tabular}
\end{table}

测试结果表明，在多核一致性方面，以及数据维护的正确性方面，320 LLC均通过了测试。同时，为了测试可能产生的死锁，额外进行了随机硬件参数测试与地址冲突请求测试。

在随机参数测试中，硬件的参数在每次测试时均不相同，有可能随机出极端的硬件参数配置，如只有1个MSHR或只有1路数据。而在地址冲突请求测试中，大量的请求集中于少数几个地址，使得请求之间的地址冲突十分严重。在这些极端情况的测试中会暴露出很多潜在的死锁问题，如未优化的调度队列会使得请求产生饿死问题，内部请求调度不合理会造成Tag阵列卡死，一致性协议边界情况未进行规定会产生协议级错误等。

本文研究的320 LLC正确地通过了所有的测试，证明了其功能的正确性。

\textbf{整体性能测试}

本文选择使用了SPEC CPU 2006测试套件作为320 LLC整体性能评估的负载，测试框架方面，本文采用了系统级测试框架进行测试，同时采用Simpoint\citep{Hamerly_Perelman_Lau_Calder_2005}技术以加速仿真过程。

\begin{table}[H]
    \bicaption{\enspace 320平台硬件参数配置}{\enspace 320 Platform Hardware Configuration}
    \label{tab:llc_config}
    \centering
    \footnotesize% fontsize
    \setlength{\tabcolsep}{4pt}% column separation
    \renewcommand{\arraystretch}{1.2}%row space 
    \begin{tabular}{@{}ccc@{}}
    部件                      & 参数名          & 参数            \\ \midrule
    \multirow{4}{*}{流水线}    & 主频           & 3 Ghz         \\
                            & 发射宽度         & 6             \\
                            & ALU数量        & 2+2+2         \\
                            & FPU数量        & 4             \\ \midrule
    \multirow{2}{*}{乱序执行模块} & ROB数量        & 256           \\
                            & 物理寄存器数       & 184           \\ \midrule
    \multirow{4}{*}{L1缓存}   & 指令缓存容量       & 128KB         \\
                            & 指令缓存MSHR数    & 32            \\
                            & 数据缓存容量       & 128KB         \\
                            & 指令缓存MSHR数    & 32+8          \\ \midrule
    \multirow{4}{*}{LLC}    & 容量           & 12MB+4MB      \\
                            & MSHR数量       & 24*4          \\
                            & 请求队列容量       & 64            \\
                            & 包含方式         & 非包含 \\ \midrule
    \multirow{4}{*}{内存}     & 仿真器          & DRAMsim3      \\
                            & 内存类型         & DDR4-3200     \\
                            & Channel-Bank & 1-8           \\
                            & RCD-CAS-RP   & 22-22-22     \\ \midrule
    \end{tabular}
    \end{table}





在仿真中，320平台硬件参数配置如表\ref{tab:llc_config}所示。



为了对测试出来的性能进行参考，本文同时对比了几个其他CPU的整体性能。用于对比的CPU包括“香山”CPU，Intel i3 10100，AMD R5 5500，Intel i7 12700K。
\begin{figure}[]
    \centering
    \includegraphics[width=1\textwidth]{320_perform}
    \bicaption{\enspace 320整体整数性能测试结果}{\enspace 320 Overall Integer Performance Test Results}
    \label{fig:320_perform}
\end{figure}

“香山”CPU为“香山”处理器的第三代“南湖”架构，主频为3Ghz，其性能数据出自“香山”团队研发记录。而Intel i3 10100为Intel的商用处理器，内部微架构为Comet Lake，主频为3.6Ghz。AMD R5 5500为AMD的商用处理器，其内部微架构为Zen3，主频为3.6GHz。Intel i7 12700K为Intel的商业处理器，本文采用其性能核（P-Core）进行测试，内部微架构为Golden Cove，主频为5.0 6GHz。


\begin{figure}[H]
    \centering
    \includegraphics[width=1\textwidth]{320_perform_float}
    \bicaption{\enspace 320整体浮点性能测试结果}{\enspace 320 Overall Float Point Performance Test Results}
    \label{fig:320_perform_float}
\end{figure}

整体系统的整数性能测试结果如图\ref{fig:320_perform}所示。从测试结果中可以看出，在系统级性能测试下，使用320 LLC作为末级缓存的320平台单核性能表现超过了类似的”香山“处理器的性能，甚至超过了商用级处理器i3 10100的性能，接近AMD R5 5500的性能表现。


浮点部分的性能测试如图\ref{fig:320_perform_float}所示，由于“香山”项目并未提供其浮点性能数据，因此在该测试中未对其进行对比。浮点性能测试结果与整数类似，“320”平台的性能超过了i3 10100的性能，接近AMD R5 5500的性能表现。



\textbf{LLC对整体性能的影响}

为了测试在320 CPU整体性能中，LLC对整体性能的影响，本文分别进行了去掉LLC，以及关闭所有320 LLC优化的对比测试，以分别测试在整体系统中LLC对性能的贡献占比，以及本文所提出的优化对性能的贡献占比。


\begin{figure}[H]
    \centering
    \includegraphics[width=1\textwidth]{320_no_cache_int}
    \bicaption{\enspace 320无LLC整数性能数据}{\enspace 320 Integer Performance Without LLC}
    \label{fig:320_no_cache_int}
\end{figure}


\begin{figure}[H]
    \centering
    \includegraphics[width=1\textwidth]{320_no_cache_float}
    \bicaption{\enspace 320无LLC浮点性能数据}{\enspace 320 Overall Float Point Performance Without LLC}
    \label{fig:320_no_cache_float}
\end{figure}


测试的结果如图\ref{fig:320_no_cache_int}与\ref{fig:320_no_cache_float}所示。

测试结果表明，末级缓存对整体性能贡献占比约为31.75\%，其中整数部分性能贡献占比为33.44\%，浮点部分性能贡献占比为30.55\%，需要注意的是，320系统中的L1缓存启用了固定步长预取器与流式预取器，因此可以掩盖部分末级缓存容量不足而产生的性能下降。而320 LLC的各项优化选项给整体带来了约4.98\%的性能提升，其中整数部分的性能提升约为8.42\%，浮点测试部分的性能提升约为2.56\%。

值得注意的是462.libquantum子项，该子项对延迟较为敏感，且访存请求在缓存内部较易发生冲突。因此在未采用请求缓冲-调度队列优化的情况下，该子项的访存请求会导致缓存内部出现大量的阻塞现象，从而导致性能反而低于无缓存时的表现。

\textbf{请求缓冲-调度队列对性能的影响}

\begin{table}[!htbp]
    \bicaption{\enspace 请求缓冲-调度队列对性能的影响}{\enspace The Impact of ReqArray on Performance}
    \label{tab:req_array_perform}
    \centering
    \begin{tabular}{@{}ll@{}}
    子项             & 性能百分比    \\ \midrule
    462.libquantum & 181.41\% \\
    410.bwaves     & 101.64\% \\
    433.milc       & 104.08\% \\
    436.cactusADM  & 101.62\% \\
    450.soplex     & 101.63\% \\
    459.GemsFDTD   & 102.25\% \\ \midrule
    geomean        & 102.50\%
    \end{tabular}

\end{table}
为了测试请求缓冲-调度队列对性能的影响，本文对请求缓冲队列对性能的影响进行了测试。在其余硬件参数不变的情况下，对比了无队列与有队列对性能产生的影响。在SPEC CPU 2006测试中，整数测试部分的子项462.libquantum受请求缓冲-调度队列的性能影响最大，而浮点测试部分共有5个子项受到了请求缓冲-调度队列的影响。性能影响大于1\%的子项测试结果列于表\ref{tab:req_array_perform}中。请求缓冲-调度队列使得系统整体性能提升了2.5\%，而在462.libquantum子项中提升了81.41\%。而发生性能下降最大的子项为400.perlbench（表中未列出），性能下降幅度为0.3\%。

% Please add the following required packages to your document preamble:
% \usepackage{booktabs}

从性能测试结果可以看出，存在以462.libquantum为代表的的请求缓冲-调度队列敏感的子项，但是同样存在着对其不敏感的负载。在对其敏感的子项中，请求缓冲-调度队列能带来明显的性能提升，而不敏感的子项中，并未造成明显的性能下降。因此在末级缓存中添加一个开销较小而效率较高的请求缓冲-调度队列有利于整体性能的提升。

\textbf{非包含式设计对性能的影响}

非包含式设计下，等效地增加了整体系统中可用的缓存容量，从而提升整体缓存的命中率。因而本文比较了非包含式与包含式设计下的命中率与MPKI（每千条指令缺失率）的影响。本文从SPEC CPU 2006的Simpoint中选择了若干个MPKI大于1的测试点，分别比较开关非包含式选项的影响。

\begin{table}[!htbp]
    \bicaption{\enspace 非包含式设计对MPKI的影响}{\enspace The Impact of Non-Incluside Design on MPKI}
    \label{tab:non_inclusive_mpki}
    \centering
    \begin{tabular}{llll}
        片段名 & \multicolumn{1}{l}{I-MPKI} & \multicolumn{1}{l}{N-MPKI} & \multicolumn{1}{l}{差值} \\ \hline
        403.gcc2\_4        & 2.94  & 2.94  & -0.01 \\
        403.gcc6\_2        & 1.16  & 1.21  & 0.05  \\
        410.bwaves1\_10    & 1.51  & 1.19  & -0.33 \\
        410.bwaves1\_17    & 2.56  & 2.36  & -0.21 \\
        434.zeusmp1\_12    & 3.39  & 3.31  & -0.07 \\
        434.zeusmp1\_17    & 1.12  & 1.20  & 0.08  \\
        436.cactusADM1\_14 & 1.62  & 1.17  & -0.44 \\
        436.cactusADM1\_6  & 1.52  & 1.10  & -0.42 \\
        445.gobmk4\_0      & 7.40  & 7.40  & 0.00  \\
        445.gobmk4\_2      & 2.93  & 2.88  & -0.05 \\
        450.soplex2\_10    & 11.97 & 11.94 & -0.03 \\
        450.soplex2\_6     & 8.00  & 7.52  & -0.49
    \end{tabular}
\end{table}

\begin{table}[!htbp]
    \bicaption{\enspace 非包含式设计对命中率的影响}{\enspace The Impact of Non-Incluside Design on HitRate}
    \label{tab:non_inclusive_hit_rate}
    \centering
    \begin{tabular}{llll}
        片段名 & \multicolumn{1}{l}{包含式命中率} & \multicolumn{1}{l}{非包含式命中率} & \multicolumn{1}{l}{差值} \\ \hline
        403.gcc2\_4        & 68.30\%   & 68.75\%   & 0.45\%        \\
        403.gcc6\_2        & 51.20\%   & 48.77\%   & -2.43\%       \\
        410.bwaves1\_10    & 95.37\%   & 96.37\%   & 1.00\%        \\
        410.bwaves1\_17    & 91.85\%   & 92.51\%   & 0.67\%        \\
        434.zeusmp1\_12    & 59.29\%   & 59.74\%   & 0.45\%        \\
        434.zeusmp1\_17    & 89.73\%   & 88.94\%   & -0.78\%       \\
        436.cactusADM1\_14 & 68.36\%   & 76.95\%   & 8.58\%        \\
        436.cactusADM1\_6  & 69.33\%   & 77.75\%   & 8.42\%        \\
        445.gobmk4\_0      & 29.84\%   & 29.91\%   & 0.07\%        \\
        445.gobmk4\_2      & 65.83\%   & 66.49\%   & 0.66\%        \\
        450.soplex2\_10    & 61.46\%   & 61.60\%   & 0.14\%        \\
        450.soplex2\_6     & 78.83\%   & 80.75\%   & 1.92\%        
    \end{tabular}
\end{table}

测试结果如表\ref{tab:non_inclusive_mpki}及表\ref{tab:non_inclusive_hit_rate}所示。从表中可以看出，非包含式设计可以降低缓存的MPKI，同时增加缓存的命中率，其中436子项最为明显，其两个测试点均减少了约0.4的MPKI，而命中率则增加了约8.5\%。

从测试结果可以看出，非包含式设计对优化MPKI与请求命中率这两个指标均有帮助，是高性能末级缓存必要的优化设计。

\textbf{同组请求并行设计对性能的影响}

在“320 LLC”的设计中，为了优化重负载情况下可能出现的组冲突问题，本文为其引入了同组并行的机制以使得这些请求可以并行执行。为了研究该机制对性能的影响，本文从测试集中选择了几个内存负载较大的测试点，并在这几个测试点上开关同组并行机制以观察性能影响。

\begin{table}[!htbp]
    \bicaption{\enspace 同组并行设计对请求处理延迟周期数的影响}{\enspace The Impact of Set-Parallel Execute Design on Latency}
    \label{tab:set_parrel_lat}
    \centering
    \begin{tabular}{@{}lllllll@{}}
    \toprule
    \multicolumn{1}{c}{\multirow{2}{*}{测试点}} & \multicolumn{3}{c}{同组并行延迟} & \multicolumn{3}{c}{同组串行延迟} \\
    \multicolumn{1}{c}{} & 平均    & 命中   & 缺失     & 平均    & 命中    & 缺失     \\ \midrule
    436.cactusADM1\_1    & 281.9 & 13.9 & 1296.3 & 300.0 & 164.2 & 866.4  \\
    436.cactusADM1\_2    & 558.1 & 40.0 & 1741.4 & 709.1 & 460.4 & 1363.2 \\
    436.cactusADM1\_3    & 493.7 & 36.1 & 1654.8 & 645.4 & 407.9 & 1300.1 \\
    436.cactusADM1\_4    & 489.3 & 34.6 & 2127.0 & 654.0 & 406.5 & 1674.2 \\
    403.gcc4\_1          & 59.9  & 14.9 & 119.7  & 60.1  & 14.9  & 119.9  \\
    403.gcc4\_2          & 13.2  & 13.1 & 81.1   & 13.2  & 13.1  & 80.7   \\
    400.perlbench1\_1    & 15.7  & 12.1 & 80.7   & 15.7  & 12.1  & 80.2   \\
    400.perlbench1\_2    & 17.1  & 12.1 & 79.0   & 17.0  & 12.1  & 77.8   \\
    483.xalancbmk1\_1    & 12.0  & 12.0 & 81.8   & 12.0  & 12.0  & 79.6   \\
    483.xalancbmk1\_2    & 14.4  & 12.0 & 61.5   & 14.4  & 12.0  & 61.9   \\ \bottomrule
    \end{tabular}
\end{table}

\begin{table}[!htbp]
    \bicaption{\enspace 同组并行设计对命中率的影响}{\enspace The Impact of Set-Parallel Execute Design on Hit Rate}
    \label{tab:set_parrell_hitrate}
    \centering
    \begin{tabular}{@{}llll@{}}
    \toprule
    \multicolumn{1}{c}{测试点} &
      \multicolumn{1}{c}{\begin{tabular}[c]{@{}c@{}}同组并行\\ 命中率\end{tabular}} &
      \multicolumn{1}{c}{\begin{tabular}[c]{@{}c@{}}同组串行\\ 命中率\end{tabular}} &
      \multicolumn{1}{c}{差值} \\ \midrule
    436.cactusADM1\_1 & 69.02\% & 72.79\% & 3.77\%  \\
    436.cactusADM1\_2 & 90.62\% & 91.29\% & 0.67\%  \\
    436.cactusADM1\_3 & 71.73\% & 73.38\% & 1.65\%  \\
    436.cactusADM1\_4 & 78.27\% & 80.48\% & 2.21\%  \\
    403.gcc4\_1       & 57.09\% & 56.94\% & -0.15\% \\
    403.gcc4\_2       & 99.90\% & 99.90\% & 0.00\%  \\
    400.perlbench1\_1 & 94.70\% & 94.70\% & 0.00\%  \\
    400.perlbench1\_2 & 92.52\% & 92.47\% & -0.05\% \\
    483.xalancbmk1\_1 & 99.95\% & 99.95\% & 0.00\%  \\
    483.xalancbmk1\_2 & 95.26\% & 95.26\% & 0.00\%  \\ \bottomrule
    \end{tabular}
\end{table}

从表\ref{tab:set_parrel_lat}和表\ref{tab:set_parrell_hitrate}可以看出，在这几个测试点中，只有436.cactusADM这一个测试点中产生了大量的同组冲突。在该测试点中，在同组串行执行的设计下，命中的请求由于被前面冲突的正在处理的缺失请求所阻塞，因而使得即便命中也有高额的延迟。而开启了同组并行执行的优化后，命中延迟有了大幅度的下降。如436.cactusADM1\_1这个片段中，命中延迟从164.2周期下降为了13.9周期。而缺失延迟方面，由于命中延迟降低，处理器核可以更快地处理数据，因而更快地发送新的请求，同时同组并行执行的设计中，大量的缺失请求会同时向内存发射，因此对内存控制器产生了较大的压力，造成缺失请求处理延迟有所上升。而在组冲突较少的片段中，同组并行执行对延迟几乎没有影响。

而从表\ref{tab:set_parrell_hitrate}中可以看出，由于同组执行时，替换算法信息的更新也由串行变为了并行更新，会损失部分的信息，在组冲突频繁的子项436.cactusADM上有平均2\%的命中率损失，而在冲突较少的子项上，对命中率的影响趋近于0。

同时，经过观察可以发现，组冲突是否频繁与命中率没有直接关联，例如403.gcc子项中的两个片段命中率分别为56\%和99\%，在这两个片段中均无明显的组冲突，命中与缺失延迟均较低。而在436.cactusADM子项中，在命中率分别为70\%和90\%的片段中均出现了明显的组冲突问题。

因此，哪怕是在缓存命中率较高的程序上也可能出现因为组冲突而损失性能的情况。同组并行执行对于提升组冲突频繁的程序性能而言十分重要，是高性能末级缓存的必要优化。


\section{小结}

本节介绍了“320 LLC”的设计与实现。针对目前缺乏RTL级缓存微架构实验平台的问题，本文进行了“320 LLC”项目的研究。

为了提升处理器在重负载下的性能表现，在设计“320 LLC”的过程中使用了诸多优化方案。首先本文优化了320 LLC的总线的设计，采用了“320”缓存总线协议来减少物理资源的占用，消除了额外写缓冲区的开销以及提升通道利用率。本文设计了高效的请求缓冲-调度队列以使得LLC可以在MSHR忙时暂存请求。本文还引入了同组并行以及非包含式设计以提升重负载下的系统性能。

测试表明，“320 LLC”能正确地维护多核的一致性，在极端的访存压力测试中仍可保证正确性，同时不发生死锁情况。在整体的性能测试中，搭载了“320 LLC”的“320 CPU”系统单核性能超过了类似的开源处理器的性能，已经达到了商业处理器水平。“320 LLC”的各项优化使得系统整体获得了4.9\%的性能提升。

}
